package ar.com.hjg.pngj;

import java.util.zip.Checksum;

/**
 * Simple immutable wrapper for basic image info.
 * <p>
 * Some parameters are redundant, but the constructor receives an 'orthogonal' subset.
 * <p>
 * ref: http://www.w3.org/TR/PNG/#11IHDR
 */
public class ImageInfo {

  /**
   * Absolute allowed maximum value for rows and cols (2^24 ~16 million). (bytesPerRow must fit in a 32bit integer,
   * though total amount of pixels not necessarily).
   */
  public static final int MAX_COLS_ROW = 16777216;

  /**
   * Cols= Image width, in pixels.
   */
  public final int cols;

  /**
   * Rows= Image height, in pixels
   */
  public final int rows;

  /**
   * Bits per sample (per channel) in the buffer (1-2-4-8-16). This is 8-16 for RGB/ARGB images, 1-2-4-8 for grayscale.
   * For indexed images, number of bits per palette index (1-2-4-8)
   */
  public final int bitDepth;

  /**
   * Number of channels, as used internally: 3 for RGB, 4 for RGBA, 2 for GA (gray with alpha), 1 for grayscale or
   * indexed.
   */
  public final int channels;

  /**
   * Flag: true if has alpha channel (RGBA/GA)
   */
  public final boolean alpha;

  /**
   * Flag: true if is grayscale (G/GA)
   */
  public final boolean greyscale;

  /**
   * Flag: true if image is indexed, i.e., it has a palette
   */
  public final boolean indexed;

  /**
   * Flag: true if image internally uses less than one byte per sample (bit depth 1-2-4)
   */
  public final boolean packed;

  /**
   * Bits used for each pixel in the buffer: channel * bitDepth
   */
  public final int bitspPixel;

  /**
   * rounded up value: this is only used internally for filter
   */
  public final int bytesPixel;

  /**
   * ceil(bitspp*cols/8) - does not include filter
   */
  public final int bytesPerRow;

  /**
   * Equals cols * channels
   */
  public final int samplesPerRow;

  /**
   * Amount of "packed samples" : when several samples are stored in a single byte (bitdepth 1,2 4) they are counted as
   * one "packed sample". This is less that samplesPerRow only when bitdepth is 1-2-4 (flag packed = true)
   * <p>
   * This equals the number of elements in the scanline array if working with packedMode=true
   * <p>
   * For internal use, client code should rarely access this.
   */
  public final int samplesPerRowPacked;

  private long totalPixels = -1; // lazy getter

  private long totalRawBytes = -1; // lazy getter

  /**
   * Short constructor: assumes truecolor (RGB/RGBA)
   */
  public ImageInfo(int cols, int rows, int bitdepth, boolean alpha) {
    this(cols, rows, bitdepth, alpha, false, false);
  }

  /**
   * Full constructor
   * 
   * @param cols Width in pixels
   * @param rows Height in pixels
   * @param bitdepth Bits per sample, in the buffer : 8-16 for RGB true color and greyscale
   * @param alpha Flag: has an alpha channel (RGBA or GA)
   * @param grayscale Flag: is gray scale (any bitdepth, with or without alpha)
   * @param indexed Flag: has palette
   */
  public ImageInfo(int cols, int rows, int bitdepth, boolean alpha, boolean grayscale,
      boolean indexed) {
    this.cols = cols;
    this.rows = rows;
    this.alpha = alpha;
    this.indexed = indexed;
    this.greyscale = grayscale;
    if (greyscale && indexed)
      throw new PngjException("palette and greyscale are mutually exclusive");
    this.channels = (grayscale || indexed) ? (alpha ? 2 : 1) : (alpha ? 4 : 3);
    // http://www.w3.org/TR/PNG/#11IHDR
    this.bitDepth = bitdepth;
    this.packed = bitdepth < 8;
    this.bitspPixel = (channels * this.bitDepth);
    this.bytesPixel = (bitspPixel + 7) / 8;
    this.bytesPerRow = (bitspPixel * cols + 7) / 8;
    this.samplesPerRow = channels * this.cols;
    this.samplesPerRowPacked = packed ? bytesPerRow : samplesPerRow;
    // several checks
    switch (this.bitDepth) {
      case 1:
      case 2:
      case 4:
        if (!(this.indexed || this.greyscale))
          throw new PngjException("only indexed or grayscale can have bitdepth=" + this.bitDepth);
        break;
      case 8:
        break;
      case 16:
        if (this.indexed)
          throw new PngjException("indexed can't have bitdepth=" + this.bitDepth);
        break;
      default:
        throw new PngjException("invalid bitdepth=" + this.bitDepth);
    }
    if (cols < 1 || cols > MAX_COLS_ROW)
      throw new PngjException("invalid cols=" + cols + " ???");
    if (rows < 1 || rows > MAX_COLS_ROW)
      throw new PngjException("invalid rows=" + rows + " ???");
    if (samplesPerRow < 1)
      throw new PngjException("invalid image parameters (overflow?)");
  }

  /**
   * returns a copy with different size
   * 
   * @param cols if non-positive, the original is used
   * @param rows if non-positive, the original is used
   * @return a new copy with the specified size and same properties
   */
  public ImageInfo withSize(int cols, int rows) {
    return new ImageInfo(cols > 0 ? cols : this.cols, rows > 0 ? rows : this.rows, this.bitDepth,
        this.alpha, this.greyscale, this.indexed);
  }

  public long getTotalPixels() {
    if (totalPixels < 0)
      totalPixels = cols * (long) rows;
    return totalPixels;
  }

  /**
   * Total uncompressed bytes in IDAT, including filter byte. This is not valid for interlaced.
   */
  public long getTotalRawBytes() {
    if (totalRawBytes < 0)
      totalRawBytes = (bytesPerRow + 1) * (long) rows;
    return totalRawBytes;
  }

  @Override
  public String toString() {
    return "ImageInfo [cols=" + cols + ", rows=" + rows + ", bitDepth=" + bitDepth + ", channels="
        + channels + ", alpha=" + alpha + ", greyscale=" + greyscale + ", indexed=" + indexed + "]";
  }

  /**
   * Brief info: COLSxROWS[dBITDEPTH][a][p][g] ( the default dBITDEPTH='d8' is ommited)
   **/
  public String toStringBrief() {
    return String.valueOf(cols) + "x" + rows + (bitDepth != 8 ? ("d" + bitDepth) : "")
        + (alpha ? "a" : "") + (indexed ? "p" : "") + (greyscale ? "g" : "");
  }

  public String toStringDetail() {
    return "ImageInfo [cols=" + cols + ", rows=" + rows + ", bitDepth=" + bitDepth + ", channels="
        + channels + ", bitspPixel=" + bitspPixel + ", bytesPixel=" + bytesPixel + ", bytesPerRow="
        + bytesPerRow + ", samplesPerRow=" + samplesPerRow + ", samplesPerRowP="
        + samplesPerRowPacked + ", alpha=" + alpha + ", greyscale=" + greyscale + ", indexed="
        + indexed + ", packed=" + packed + "]";
  }


  void updateCrc(Checksum crc) {
    crc.update((byte) rows);
    crc.update((byte) (rows >> 8));
    crc.update((byte) (rows >> 16));
    crc.update((byte) cols);
    crc.update((byte) (cols >> 8));
    crc.update((byte) (cols >> 16));
    crc.update((byte) (bitDepth));
    crc.update((byte) (indexed ? 1 : 2));
    crc.update((byte) (greyscale ? 3 : 4));
    crc.update((byte) (alpha ? 3 : 4));
  }

  @Override
  public int hashCode() {
    final int prime = 31;
    int result = 1;
    result = prime * result + (alpha ? 1231 : 1237);
    result = prime * result + bitDepth;
    result = prime * result + cols;
    result = prime * result + (greyscale ? 1231 : 1237);
    result = prime * result + (indexed ? 1231 : 1237);
    result = prime * result + rows;
    return result;
  }

  @Override
  public boolean equals(Object obj) {
    if (this == obj)
      return true;
    if (obj == null)
      return false;
    if (getClass() != obj.getClass())
      return false;
    ImageInfo other = (ImageInfo) obj;
    if (alpha != other.alpha)
      return false;
    if (bitDepth != other.bitDepth)
      return false;
    if (cols != other.cols)
      return false;
    if (greyscale != other.greyscale)
      return false;
    if (indexed != other.indexed)
      return false;
    if (rows != other.rows)
      return false;
    return true;
  }
}
